<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Strict//EN">
<!--
//
// Copyright 2007 Qianyan Cai
// Under the terms of The GNU General Public License version 2
//
-->
<html>
<head>
<meta name="Content-Type" content="text/html; charset=UTF-8" http-equiv="Content-Type">
<title>Objot 版本14 指南</title>
<style>
	body	{ padding:0; margin:1em; background:white; line-height:1.4; font-family:sans-serif; }
	h2		{ margin:3em 0 1em; padding:0.2ex 0; border-bottom:0.3ex solid #cdf; color:#730; }
	h3		{ margin:2em 0 1em 1em; padding:0; color:#015; }
	h4		{ margin:2em 0 1em 2em; padding:0; color:#015; }
	a:link		{ color: #04e; text-decoration:none; }
	a:visited	{ color: #60c; text-decoration:none; }
	a:hover		{ background:#ddf; }
	table	{ margin:0 3ex 1em; background:#f6f6f6; }
	td		{ padding:0 1ex 0.3ex; border-bottom:1px solid #ee9; }
	pre		{ margin:1ex 2ex; padding:0.6ex; border:1px solid #cc6; background:#f2f2f2; line-height:1; }
</style>
</head>
<body>


<!--@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@-->


<h1	align=center><a href=http://objot.info>Objot</a> 版本14 指南</h1>
<hr>

<p>这是一个了解Objot，了解如何利用Objot开发浏览端的指南。
关于Java服务端开发请参考<a href=guide-java-zh.html>《Objot 版本14 Java服务端指南》</a>。

<p>版权所有2007蔡倩彦，遵循GNU General Public License version 2。


<!--@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@-->


<h2>面向服务的AJAX</h2>

<p>Objot是一个轻量级的库，可以为Web开发项目有效地构建面向服务的AJAX架构。
服务器部署业务数据，部署业务逻辑Service；浏览器部署视图，部署操作逻辑，并与Service交互。
交互的数据、流程由开发者定义，用Objot实现。
注意，Objot是库不是框架，可以根据需要和其他框架组合。

<p>Objot包含了一组Javascript、Java和Python代码，来帮助开发者实现浏览端与服务端的交互。
还包含了一组Javascript代码，来帮助开发者实现视图和操作逻辑。
在浏览端，Objot中的函数名均以$开头；在服务端，Objot代码都在objot包（模块）中。
浏览端，Objot兼容Firefox 2.0，Firefox 1.5，IE 7（测试中），IE 6；服务端，Objot要求Java 5(1.5)以上，或者Python 2.4以上。

<p>一个在线聊天的范例，演示了如何用Objot和面向服务的AJAX架构实现一个完整的Web应用。
部署此范例浏览端时，可以将objot.web目录对应为/objotweb/。


<!--@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@-->


<h2>服务对象Service Object</h2>

<p>借助于Objot轻量级对象编码解码机制，浏览端和服务端之间的数据交互简洁而有效。
可以在交互中传递的数据被称为服务对象Service Object（简称SO），允许的类型包括：
<table>
	<tr><th>Javascript<th>Java<th>Python</tr>
	<tr><td>null undefined<td>null<td>None</tr>
	<tr><td>boolean<td>boolean<td>bool</tr>
	<tr><td>number<td>int long(部分) float double Integer Long(部分) Float Double<td>int long(部分) float</tr>
	<tr><td>string<td>String<td>unicode str(UTF-8)</tr>
	<tr><td>Date<td>Date<td>datetime</tr>
	<tr><td>Array<td>boolean[] int[] long[] &lt;T&gt;T[] List&lt;T&gt; Set&lt;T&gt;<td>list tuple</tr>
	<tr><td>Object(非Function String Number Boolean)<td>Object Map&lt;String,Object&gt;<td>object(非instance) dict</tr>
</table>
其中，字符串是Unicode格式（Python服务端可以配置为UTF-8，但不推荐），并且不能包含一个特殊字符：'\20'（即\u0010）。
Objot将SO编码成一个字符串，其中使用了特殊字符'\20'。

<p>list、object之间可以任意引用，构成的整个SO图都可以被编解码；同时也可以定义规则，只处理SO图的局部（子图）。
Objot支持单继承类，服务端和浏览端可以使用不同的类名，可以使用不同的属性名，均可以对应。
传递的SO必须以list或者object为起点。

<!--****************************************************************************************-->

<h3>编解码Service Object</h3>

<p>浏览端Service Object需要定义相应的类。
例如定义Ok和Err表示服务成功和失败，ErrUnsigned表示服务需要用户登入验证：
<pre>Ok = function (hint) {
	this.hint = $(hint);
}
Err = function (hint) {
	this.hint = $(hint);
}
ErrUnsigned = function (hint) {
	this.hint = $(hint);
}
$class('Ok'), $class('Err');
$class('ErrUnsigned', Err); // ErrUnsigned的父类是Err
</pre>
3个function定义了构造函数，$class把这些function定义为类。

<p>使用$get可以将数据编码为字符串：
<pre>ok = new Ok("I'm Ok");
_ = $get(ok, Object);
</pre>
$get可以理解为get from object，得到的字符串可以再被解码为数据：
<pre>ok2 = $set(_);
ok.hint == ok2.hint || $throw('error'); // 如果hint不同，就抛出异常
</pre>
$set可以理解为set to object。
注意，解码时创建对象不调用$class中定义的构造函数，而是直接生成对象（空构造函数）。

<!--****************************************************************************************-->

<h3>编解码Service Object图</h3>

<p>由SO相互引用构成的SO图同样可以传递。
例如定义User表示用户信息，包括用户的好友：
<pre>User = function (name, email) {
	this.name = name;
	this.email = email;
	this.friends;
}
</pre>
可以创建许多User对象，并设置它们的friends，然后编码：
<pre>u1 = new User('u1', 'u1@u1.u1');
u2 = new User('u2');
... // 多创建几个
u1.friends = [ u1, u2, ... ]; // 广交朋友
u2.friends = [ ... ]; // 四海皆兄弟
y = $get(u1, Object); // 以u1为起点，编码整个SO图
</pre>
解码之后，得到和原来一样的SO图：
<pre>v1 = $set(y);
u1.name == v1.name &amp;&amp; u1.email == v1.email
	&amp;&amp; u1.friends[0].name == v1.friends[0].name &amp;&amp; ... // 完整检查
	|| $throw('error');
</pre>

<!--****************************************************************************************-->

<h3>在浏览端编码Service Object子图</h3>

<p>通常不需要将整个SO图都编码，那么可以定义一些规则，使Objot只编码SO子图。
首先定义一些function（也是构造函数）：
<pre>function DoUser() {}
DoUser.all = function () {};
</pre>
然后设置SO类与这些function对应时哪些属性被编码：
<pre>$class.get(User, Object, ['name'], DoUser, ['name', 'friends'], DoUser.all, null);
</pre>
用$class.get为User这个SO类设置规则：与Object对应时，只编码name属性；与DoUser对应时，编码name和friends属性；与DoUser.all对应时，编码所有属性；与这些都不对应时，不编码任何属性。

<p>在编码时给出需要对应的function：
<pre>$get(u1, Object); // 只编码u1的name属性
$get(u1, DoUser); // 编码u1的name和friends属性，并对friends中每个User运用同样的规则，包含整个SO图
$get(u1, DoUser.all); // 编码u1的全部属性，以及整个SO图
</pre>
排除某些引用属性，就可以排除这些引用构成的SO图。

<p>修改规则，设置某些临时引用，使得某些SO的某些引用被编码：
<pre>$class.get(User, Object, ['name'], DoUser, ['name', 'myFriends'], DoUser.all, null);
u1.myFriends = u1.friends; // u1的myFriends属性与friends属性相同，但其他User没有myFriends属性
$get(u1, DoUser);
delete u1.myFriends; // 清除临时属性
</pre>
这样编码了u1的name和myFriends属性，因为其他User没有myFriends，所以编码的SO子图就是u1自己，以及u1的直接好友。

<!--****************************************************************************************-->

<h3>服务端Service Object</h3>

<p>请参考<a href=guide-java-zh.html>《Objot 版本14 Java服务端指南》</a>。


<!--@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@-->


<h2>服务Service</h2>

<p>Objot的SO编解码机制本身独立于业务功能，实际项目可以各种方式使用这个机制并受益。
清晰地定义浏览端和服务端之间的交互流程、逻辑通常是比较好的做法。
包括：
<ul>
	<li>服务端提供哪些服务Service。
	<li>Service需要什么SO，SO要满足什么条件。
	<li>Service完成哪些工作，有哪些结果。
	<li>根据不同结果，Service返回什么SO，满足什么条件。
	<li>Service间有哪些相互关系（例如先后顺序）。
	<li>Service如何处理并发请求（例如一次Service一次数据库事务）。
	<li>等等。
</ul>
浏览端与Service交互的部分可以称为Service Client（下称Client）。
Client与Service交互通常借助异步HTTP来完成，考虑：
<ul>
	<li>需要哪些Service。
	<li>如何构造传递给Service的SO，并满足条件。
	<li>接收到Service结果，根据不同的SO进行哪些处理。
	<li>因为是异步HTTP，如何保证并发调用Service后，处理Service结果仍然正确。
	<li>等等。
</ul>

<!--****************************************************************************************-->

<h3>浏览端Service Client</h3>

<p>Objot通过$http函数提供异步HTTP功能：
<pre>http = $http(url, timeout, req, done, data);
</pre>
req是请求内容字符串，可以是$get编码串，$http总是用POST方式发出请求，这样不需要格式转换，长度也不受限制。
timeout&gt;0是超时毫秒数，否则忽略不做超时处理。
done是HTTP回合结束时异步回调的函数，可能是回合完成，可能是超时，也可能是停止。
$http返回一个无参数函数，用来停止这个回合，并异步调用done，如果交互已经结束，则不起作用。

<p>回调函数done例如：
<pre>done = function (code, res, data) {
	alert(code + '\n' + res + '\n' + data);
}
// 然后调用$http
</pre>
code是一个整数：0表示回合成功（HTTP响应码200），1表示超时，-1表示停止，100~999表示HTTP响应码，&gt;=1000通常表示网络错误。
当code == 0时res是服务端返回的内容字符串，其他情况则是错误信息字符串。
data是调用$http的data参数，可以给done传递额外对象，如果调用$http省略data参数，则$http返回的停止函数将做为data传给done。
每次HTTP回合必调用done正好一次，除非没设置超时，或者跳转到别的页面，或者Javascript引擎停止了;)。

<p>Objot还提供了对$http的一个简单包装：
<pre>http = $Do(url, hint, req, this3, done3, this2, done2, this1, done1);
</pre>
hint是对这次HTTP回合的提示字符串，可以在视图上表示出来。
done3~1是回调函数，this3~1是回调时设置的this对象，done1先于done3回调。
$Do返回值和$http相同，但附加了一些以$开头为名的属性用作内部处理。

<p>done3~1回调函数和$http有所不同：
<pre>done3 = function (ok, err, http) {
	alert(ok + '\n' + err + '\n' + http);
}
</pre>
若回合成功完成（服务器返回200），ok为$set解码的SO，err为false；若超时或错误，ok为false，err为前面定义的Err对象；若是停止，ok和err均为false。
http是$Do也是$http的返回值。
当某个回调出现错误，将传递给Javascript引擎，后续回调函数就不执行了，依此类推。

<!--****************************************************************************************-->

<h3>直观的HTTP交互</h3>

<p>Objot提供两个简单的函数，在视图上显示与服务端的交互：
<pre>box = $s(); // 创建一个span节点
$dom($D.body).add(box); // 加到body中
box == $Http(box, do_); // 注意是$Http
</pre>
$Http在box中创建一个span节点，通常在css中定义背景图片表示正在网络操作。
do_是$Do的返回值。
当用户双击box中的span节点（通常有图标），就触发停止函数，并使span节点消失。
$Http返回值就是传入的box。
<pre>box == $Err(box, err, show);
</pre>
$Err在box中创建一个span节点，通常在css中定义背景图片表示出错。
err是前面定义的Err对象，或者错误信息字符串。
show如果为true，则在box中显示err文本内容；否则为一个回调函数，此时box中不显示文本内容，但用户双击span节点调用show，返回后span节点消失；若为null或省略，则为默认的一个回调函数。
$Err返回值就是传入的box。

<p>对于这两个函数，可以使用同一个box，调用时会清除掉原来box中的内容：
<pre>http = $s();
$dom($D.body).add(http);
...
$Http(http, $Do(url, 'Signing in ...', signIn, null, function (ok, err) {
	if (ok)
		; // 成功处理
	err &amp;&amp; $Err(http, err);
}));
</pre>

<!--****************************************************************************************-->

<h3>并发HTTP交互</h3>

<p>每次调用$http都会产生一个独立的HTTP回合，与其他回合并发处理。
由于Javascript是单线程运行，并发异步回合会导致回调函数的顺序不确定，例如：
<pre>x = '';
function http() {
	a = $http(..., done), x += a.x = 'a';
	b = $http(..., done), x += b.x = 'b';
	c = $http(..., done), x += c.x = 'c';
}
function done(code, res, data) {
	x += data.x;
}
http();
</pre>
那么最后x是'abcabc'，'abccab'，'abcbca'等许多可能。

<p>回调顺序不确定，Javascript本身又没有事务支持，因此常用策略是请求时只使用局部或对象数据，而在回调时才将局部或对象数据应用到更大范围：
<pre>_user = new User();
DoUser = function () {}
DoUser.update = function (friends, This, done) {
	var u = new User();
	u.friends = friends;
	var h = $Do('DoUser-update', 'Updating my info', $get(u, this.update),
		This, done, null, this.doneUpdate);
	h.user = u;
	return h;
}
	DoUser.doneUpdate = function (ok, err, h) {
		ok &amp;&amp; (_user.friends = h.user.myFriends);
	}
</pre>
DoUser.update中，先生成一个临时数据u，并设置为$Do返回函数的属性。
DoUser.doneUpdate中，如果ok，才将临时数据u合并入全局数据_user。
临时数据也可以作为对象传入$Do：
<pre>	...
	var h = $Do('DoUser-update', 'Updating my info', $get(u, this.update),
		This, done, u, this.doneUpdate);
	...
}
	DoUser.doneUpdate = function (ok, err, u) {
		ok &amp;&amp; (_user.friends = u.myFriends);
	}
</pre>
这样DoUser.doneUpdate可以直接得到这个临时数据。


<!--@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@-->


<h2>视图View</h2>

<p>显示视图有多种方式，Objot建议采用面向对象的思路，对视图的逻辑处理比较容易，也易于划分视图组件，减少耦合。

<!--****************************************************************************************-->

<h3>创建并使用dom节点</h3>

<p>Objot对HTML DOM做了一些简单包装：
<pre>var d = $dom('div'); // 创建&lt;div&gt;&lt;/div&gt;
d = $dom('div', 'c', 'cssClass1'); // 创建&lt;div class=cssClass1&gt;&lt;/div&gt;
d = $dom('div', 's', 'display:none', 'c', 'cssClass2');
	// 创建&lt;div class=cssClass2 style=display:none&gt;&lt;/div&gt;
d = $dom('div', 'onclick', function () { alert(this); });
	// 创建&lt;div&gt;&lt;/div&gt;并设置click事件函数
d = $d(); // $dom('div')的简写
d = $d('c', 'cssClass1');
in = $inp('type', 'checkbox', 'checked', true);
	// 创建&lt;input type=checkbox checked&gt;
</pre>
$dom根据tag名称、属性名、属性值创建节点，如果属性值是function，就作为事件函数（注意$d('onclick', 'alert(this)')不合适）。
$d、$p、$s、$a、$inp等都是常用节点的简写，参见源代码。
除了给出tag名，还可以是某个dom节点：
<pre>var d = $id('abc'); // 根据id取得一个节点
d == $dom(d, 'c', 'cssClass1'); // 设置这个节点css class为cssClass1
d == $dom(d, 's', 'display:none'); // 再做些设置
</pre>
$dom等返回值就是html dom节点本身，无其他封装对象。

<p>除了设置属性，$dom还给节点增加一些函数：
<pre>d.tx(d.tx() + 'line1 \n multi whitespaces  line2&amp;nbsp;', true);
	// 取得文本内容，再设置多行文本内容
d.add(0, $tx('single line'), $l());
	// 创建文本节点（非$dom性态）和&lt;label&gt;&lt;/label&gt;，添加到div第一个节点之前
d.add($a0(), $bn());
	// 创建&lt;a href=javascript://&gt;&lt;/a&gt;和&lt;button&gt;&lt;/button&gt;，添加到div末尾
d.rem(1); // 将第二个节点清除
d.des(0); // 将第一个节点递归销毁
d.cla('cssClass2').cla(0, 'cssClass1'); // 添加cssClass2，然后清除cssClass1
d.att('title', $dom(d, 'title', 'A div').att('title') + '...');
	// 先直接设置对象属性title，再用getAttribute()取得title，再用setAttribute设置title
d.show(true).attach('onclick', onClick); // 清除display:none，再设置事件函数
d.detach('onclick', onClick).show(false); // 清除事件函数，再设置display:none
d.des(); // 从父节点中清除自己，销毁自己，并递归销毁所有子节点
</pre>
无参数时att(), tx(), show()返回获取的值，有参数时，以及其他函数，都返回节点自身。

<p>给节点设置的事件函数，其this对象默认是节点自身，也可以定义为其他对象：
<pre>$this(d, new Date());
</pre>
这样节点的事件函数的this对象都是一个时间对象。事件函数的第一个参数是一个Event对象：
<pre>function onClick(e) {
	alert(e.target); // 产生事件的直接节点，可能不是设置事件函数的节点（bubble机制）
	e.stop(); // 停止bubble
}
</pre>

<!--****************************************************************************************-->

<h3>视图组件</h3>

<p>结合Objot和Javascript面向对象机制，构造视图组件就比较容易：
<pre>_SignIn = function (box) {
	this.box = box.add(this.err = $p(), // &lt;...&gt;... &lt;p&gt;&lt;/p&gt;
		$p().add($l().tx('User name'), this.name = $ln()),
			// &lt;p&gt;&lt;label&gt;User name&lt;/label&gt;&lt;input type=text&gt;&lt;/p&gt;
		$p().add($l().tx('Password'), this.pass = $inp('type', 'password')),
			// &lt;p&gt;&lt;label&gt;Password&lt;/label&gt;&lt;input type=password&gt;&lt;/p&gt;
		$p('s', 'text-align:center').add( // &lt;p style="text-align:center"&gt;
			this.submit = $this($bn('onclick', this.doSign), this).tx('Signin / Signup'),
				// &lt;button&gt;Signin / Signup&lt;/button&gt;并绑定onclick事件
			this.http = $s()) // &lt;span&gt;&lt;/span&gt;&lt;/p&gt;
	);
	this.name.focus();
}
</pre>
这是一个登入视图组件，生成一些dom节点。并且设置一些事件函数，定义为：
<pre>_SignIn.prototype = {
	doSign: function () {
		this.submit.disabled = true, this.submit.blur();
		$Http(this.http, $Do('DoSign-inUp', 'Signing',
			$get(new User(null, this.name.value, this.pass.value)), this, this.doneSign));
			// 访问service，提供一个User作为SO
	},
	doneSign: function (ok, err) {
		this.submit.disabled = false;
		ok &amp;&amp; this.onOk.call(this.thisOk); // 如果成功，回调onOk
		err &amp;&amp; $Err(this.err, err, true); // 如果错误，显示信息
	}
}
_SignIn.prototype.thisOk; // onOk中的this对象，由组件使用者设置
_SignIn.prototype.onOk; // 登入成功后的处理，由组件使用者设置
</pre>
可以这样应用这个组件：
<pre>&lt;body&gt;
&lt;div id=SignIn1&gt;&lt;/div&gt;&lt;div id=SignIn2&gt;&lt;/div&gt;
&lt;script&gt;function onload() {
	var s1 = $id('SignIn1');
	new _SignIn(s1, null, function () {
		s1.des(); // 销毁SignIn1，就清除了对_SignIn组件对象的引用，便于垃圾回收
	});
	var s2 = $id('SignIn2');
	new _SignIn(s2, null, function () {
		s2.des();
	}); // 可以创建多个组件对象
}
&lt;/script&gt;
</pre>


<!--@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@-->


</body>
</html>
